<?php

declare(strict_types=1);

namespace app\service\notify;

use app\model\NotifyMessage;
use app\model\NotifyRecipient;
use app\model\User;
use app\service\user\AuthService;
use InvalidArgumentException;
use think\Paginator;

class UserNotificationService
{
    public function __construct(
        private readonly AuthService $authService,
        private readonly RecordService $recordService
    ) {
    }

    public function summary(User $user): array
    {
        $personalPairs = $this->resolvePersonalPairs($user);
        $groupPairs = $this->resolveGroupPairs($user);
        $allPairs = $this->mergePairs($personalPairs, $groupPairs);

        return [
            'personal_unread' => $this->countUnread($personalPairs),
            'group_unread' => $this->countUnread($groupPairs),
            'total_unread' => $this->countUnread($allPairs),
        ];
    }

    public function paginatePersonal(User $user, int $page, int $pageSize): array
    {
        $pairs = $this->resolvePersonalPairs($user);
        return $this->paginateByPairs($pairs, $page, $pageSize);
    }

    public function paginateGroup(User $user, int $page, int $pageSize): array
    {
        $pairs = $this->resolveGroupPairs($user);
        return $this->paginateByPairs($pairs, $page, $pageSize);
    }

    public function paginateAll(User $user, int $page, int $pageSize): array
    {
        $personal = $this->resolvePersonalPairs($user);
        $group = $this->resolveGroupPairs($user);
        $pairs = $this->mergePairs($personal, $group);
        return $this->paginateByPairs($pairs, $page, $pageSize);
    }

    public function markAsRead(User $user, int $messageId, int $recipientId): array
    {
        /** @var NotifyRecipient|null $recipient */
        $recipient = NotifyRecipient::with(['message', 'message.template'])
            ->where('id', $recipientId)
            ->where('message_id', $messageId)
            ->find();

        if (!$recipient) {
            throw new InvalidArgumentException('通知不存在或已失效');
        }

        if (!$this->recipientBelongsToUser($user, $recipient)) {
            throw new InvalidArgumentException('无权操作该通知');
        }

        $updated = $this->recordService->markAsRead(
            (int) $recipient->message_id,
            (int) $recipient->target_id,
            (string) $recipient->target_type,
            (int) $recipient->id
        );

        $recipientMap = $this->resolveRecipientsMap([$recipient]);
        $updatedModel = $recipientMap[(int) $recipient->id] ?? null;
        if (!$updatedModel) {
            throw new InvalidArgumentException('通知不存在或已失效');
        }

        return $this->formatRecipient($updatedModel);
    }

    protected function paginateByPairs(array $pairs, int $page, int $pageSize): array
    {
        if ($page < 1) {
            $page = 1;
        }
        if ($pageSize <= 0) {
            $pageSize = 10;
        }
        if ($pairs === []) {
            return [
                'list' => [],
                'pagination' => [
                    'total' => 0,
                    'page' => $page,
                    'page_size' => $pageSize,
                ],
                'unread' => 0,
            ];
        }

        $query = $this->buildRecipientQuery($pairs);

        /** @var Paginator $paginator */
        $paginator = $query->paginate([
            'list_rows' => $pageSize,
            'page' => $page,
        ]);

        $rawItems = $paginator->items();
        $recipientMap = $this->resolveRecipientsMap($rawItems);

        $items = [];
        foreach ($rawItems as $recipient) {
            $id = $this->extractRecipientId($recipient);
            if ($id && isset($recipientMap[$id])) {
                $items[] = $this->formatRecipient($recipientMap[$id]);
            }
        }

        return [
            'list' => $items,
            'pagination' => [
                'total' => (int) $paginator->total(),
                'page' => (int) $paginator->currentPage(),
                'page_size' => (int) $paginator->listRows(),
            ],
            'unread' => $this->countUnread($pairs),
        ];
    }

    /**
     * @param iterable<int, mixed> $items
     * @return array<int, NotifyRecipient>
     */
    protected function resolveRecipientsMap(iterable $items): array
    {
        $ids = [];
        foreach ($items as $item) {
            $id = $this->extractRecipientId($item);
            if ($id) {
                $ids[$id] = $id;
            }
        }

        if ($ids === []) {
            return [];
        }

        $collection = NotifyRecipient::with(['message', 'message.template'])
            ->whereIn('id', array_values($ids))
            ->select();

        $map = [];
        foreach ($collection as $recipient) {
            if ($recipient instanceof NotifyRecipient) {
                $map[(int) $recipient->id] = $recipient;
            }
        }

        return $map;
    }

    protected function extractRecipientId(mixed $recipient): ?int
    {
        if ($recipient instanceof NotifyRecipient) {
            return (int) $recipient->id;
        }

        if ($recipient instanceof \think\Model) {
            return (int) $recipient->getAttr('id');
        }

        if (is_array($recipient)) {
            return isset($recipient['id']) ? (int) $recipient['id'] : null;
        }

        if (is_numeric($recipient)) {
            $value = (int) $recipient;
            return $value > 0 ? $value : null;
        }

        return null;
    }

    protected function buildRecipientQuery(array $pairs)
    {
        $query = NotifyRecipient::with(['message', 'message.template'])
            ->order('id', 'desc');

        $query->where(function ($subQuery) use ($pairs) {
            foreach ($pairs as $pair) {
                $subQuery->whereOr(function ($row) use ($pair) {
                    $row->where('target_type', $pair['target_type'])
                        ->where('target_id', $pair['target_id']);
                });
            }
        });

        return $query;
    }

    protected function countUnread(array $pairs): int
    {
        if ($pairs === []) {
            return 0;
        }

        $query = NotifyRecipient::where(function ($subQuery) use ($pairs) {
                foreach ($pairs as $pair) {
                    $subQuery->whereOr(function ($row) use ($pair) {
                        $row->where('target_type', $pair['target_type'])
                            ->where('target_id', $pair['target_id']);
                    });
                }
            })
            ->whereNotIn('status', [
                NotifyRecipient::STATUS_READ,
                NotifyRecipient::STATUS_CANCELLED,
            ]);

        return (int) $query->count();
    }

    protected function resolvePersonalPairs(User $user): array
    {
        $allPairs = $this->resolveAllPairs($user);
        $personal = array_values(array_filter($allPairs, function (array $pair) use ($user) {
            return $this->isPersonalPair($pair, (int) $user->id);
        }));

        if ($personal === []) {
            $personal[] = [
                'target_type' => 'user',
                'target_id' => (int) $user->id,
            ];
        }

        return $this->uniquePairs($personal);
    }

    protected function resolveGroupPairs(User $user): array
    {
        $allPairs = $this->resolveAllPairs($user);
        $groupPairs = array_values(array_filter($allPairs, function (array $pair) use ($user) {
            return !$this->isPersonalPair($pair, (int) $user->id);
        }));

        return $this->uniquePairs($groupPairs);
    }

    protected function resolveAllPairs(User $user): array
    {
        $groups = $this->authService->getGatewayGroups($user);
        $pairs = [];

        if (is_array($groups)) {
            foreach ($groups as $group) {
                $pair = $this->parseGroupName((string) $group);
                if ($pair !== null) {
                    $pairs[] = $pair;
                }
            }
        }

        return $this->uniquePairs($pairs);
    }

    protected function mergePairs(array ...$collections): array
    {
        $merged = [];
        foreach ($collections as $collection) {
            foreach ($collection as $pair) {
                $merged[] = $pair;
            }
        }

        return $this->uniquePairs($merged);
    }

    protected function uniquePairs(array $pairs): array
    {
        $unique = [];
        foreach ($pairs as $pair) {
            $type = isset($pair['target_type']) ? strtolower((string) $pair['target_type']) : '';
            $id = array_key_exists('target_id', $pair) && $pair['target_id'] !== null
                ? (int) $pair['target_id']
                : 0;
            if ($type === '') {
                continue;
            }
            $key = $type . ':' . $id;
            $unique[$key] = [
                'target_type' => $type,
                'target_id' => $id,
            ];
        }

        return array_values($unique);
    }

    protected function parseGroupName(string $group): ?array
    {
        if ($group === '') {
            return null;
        }

        $separator = strrpos($group, '_');
        if ($separator === false) {
            return [
                'target_type' => strtolower($group),
                'target_id' => 0,
            ];
        }

        $type = substr($group, 0, $separator);
        $idPart = substr($group, $separator + 1);

        if ($type === '') {
            return null;
        }

        if ($idPart === '') {
            return [
                'target_type' => strtolower($type),
                'target_id' => 0,
            ];
        }

        if (!ctype_digit($idPart)) {
            return null;
        }

        return [
            'target_type' => strtolower($type),
            'target_id' => (int) $idPart,
        ];
    }

    protected function isPersonalPair(array $pair, int $userId): bool
    {
        $type = strtolower((string) ($pair['target_type'] ?? ''));
        $targetId = isset($pair['target_id']) ? (int) $pair['target_id'] : 0;
        if ($targetId <= 0) {
            return false;
        }

        if ($type === 'user' && $targetId === $userId) {
            return true;
        }

        if ($targetId === $userId && str_ends_with($type, '_user')) {
            return true;
        }

        return false;
    }

    protected function recipientBelongsToUser(User $user, NotifyRecipient $recipient): bool
    {
        $allPairs = $this->resolveAllPairs($user);
        foreach ($allPairs as $pair) {
            if ($pair['target_type'] === strtolower((string) $recipient->target_type)
                && $pair['target_id'] === (int) $recipient->target_id) {
                return true;
            }
        }

        return false;
    }

    protected function formatRecipient(NotifyRecipient $recipient): array
    {
        $formatted = $this->recordService->formatRecipient($recipient);

        $message = $recipient->message;
        $payload = [];
        $route = '';
        $sentAt = null;

        if ($message instanceof NotifyMessage) {
            $payload = $this->normalisePayload($message->payload);
            $route = isset($payload['route']) ? (string) $payload['route'] : '';
            $sentAt = $message->sent_at ?? $message->create_time;
            $formatted['message'] = [
                'id' => (int) $message->id,
                'title' => $message->title,
                'content' => $message->content,
                'payload' => $payload,
                'sent_at' => $message->sent_at,
                'create_time' => $message->create_time,
                'update_time' => $message->update_time,
            ];

            if ($message->template) {
                $formatted['message']['template'] = [
                    'id' => (int) $message->template->id,
                    'name' => $message->template->name,
                    'type' => $message->template->type,
                ];
            } else {
                $formatted['message']['template'] = null;
            }
        } else {
            $formatted['message'] = null;
        }

        $formatted['title'] = $formatted['message']['title'] ?? '';
        $formatted['content'] = $formatted['message']['content'] ?? '';
        $formatted['payload'] = $payload;
        $formatted['route'] = $route;
        $formatted['sent_at'] = $sentAt;
        $formatted['is_read'] = (int) $recipient->status === NotifyRecipient::STATUS_READ
            && !empty($recipient->read_time);

        return $formatted;
    }

    protected function normalisePayload(mixed $payload): array
    {
        if (is_array($payload)) {
            return $payload;
        }

        if (is_string($payload) && $payload !== '') {
            $decoded = json_decode($payload, true);
            if (is_array($decoded)) {
                return $decoded;
            }
        }

        return [];
    }
}
